前面两篇已经为大家介绍了golang中的日志如何使用,并在诸多日志框架库中选择了zap作为我们的日志框架,本篇将会讲解:
- 如何结合当下主流的Web框架gin进行请求日志的打印
- 对zap进行二次封装,注入trace信息,一遍我们可以在业务中查询一次请求的所有完整日志
这里是前两篇的链接:
1、gin 默认的中间件
首先我们来看一个最简单的 gin 项目:
func main() {
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
c.String("hello jianfan.com!")
})
r.Run(
}
接下来我们看一下gin.Default()
的源码:
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
也就是我们在使用gin.Default()
的同时是用到了 gin 框架内的两个默认中间件Logger()
和Recovery()
。
其中Logger()
是把 gin 框架本身的日志输出到标准输出(我们本地开发调试时在终端输出的那些日志就是它的功劳),而Recovery()
是在程序出现 panic 的时候恢复现场并写入 500 响应的。
2、基于 zap 的中间件
gin框架支持用户自定义的middleware,我们可以模仿Logger()
和Recovery()
的实现,使用我们的日志库来接收 gin 框架默认输出的日志。
这里以 zap 为例,我们实现两个中间件如下:
// GinLogger 接收gin框架默认的日志
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
cost := time.Since(start)
logger.Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
// GinRecovery recover掉项目可能出现的panic
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
如果不想自己实现,可以使用 github 上有别人封装好的 [https://github.com/gin-contrib/zap_](https://github.com/gin-contrib/zap)_。
这样我们就可以在 gin 框架中使用我们上面定义好的两个中间件来代替 gin 框架默认的Logger()
和Recovery()
了。
r := gin.New()
r.Use(GinLogger(), GinRecovery())
3、增加Trace信息
3.1 定义trace信息
// 定义trace结构体
type Trace struct {
TraceId string `json:"trace_id"`
SpanId string `json:"span_id"`
Caller string `json:"caller"`
SrcMethod *string `json:"srcMethod,omitempty"`
UserId int `json:"user_id"`
}
// 根据gin的context获取context,使log trace更加通用
func GetTraceCtx(c *gin.Context) context.Context {
return c.MustGet(consts.TraceCtx).(context.Context)
}
3.2 包装基于zap的日志工具
- 包装方法将context传入
- 解析trace信息,打印到logger中
package log
import (
"best-practics/common"
"best-practics/common/consts"
"context"
"go.uber.org/zap"
)
type LogWrapper struct {
logger *zap.Logger
}
var Log LogWrapper
func Debug(tag string, fields ...zap.Field) {
Log.logger.Debug(tag, fields...)
}
func DebugF(ctx context.Context, tag string, fields ...zap.Field) {
trace := ctx.Value(consts.TraceKey).(*common.Trace)
Log.logger.Debug(tag,
append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
)
}
func Info(tag string, fields ...zap.Field) {
Log.logger.Info(tag, fields...)
}
func InfoF(ctx context.Context, tag string, fields ...zap.Field) {
trace := ctx.Value(consts.TraceKey).(*common.Trace)
Log.logger.Info(tag,
append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
)
}
func Warn(tag string, fields ...zap.Field) {
Log.logger.Warn(tag, fields...)
}
func WarnF(ctx context.Context, tag string, fields ...zap.Field) {
trace := ctx.Value(consts.TraceKey).(*common.Trace)
Log.logger.Warn(tag,
append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
)
}
func Error(tag string, fields ...zap.Field) {
Log.logger.Error(tag, fields...)
}
func ErrorF(ctx context.Context, tag string, fields ...zap.Field) {
trace := ctx.Value(consts.TraceKey).(*common.Trace)
Log.logger.Error(tag,
append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
)
}
func Fatal(tag string, fields ...zap.Field) {
Log.logger.Fatal(tag, fields...)
}
func FatalF(ctx context.Context, tag string, fields ...zap.Field) {
trace := ctx.Value(consts.TraceKey).(*common.Trace)
Log.logger.Fatal(tag,
append(fields, zap.String("trace_id", trace.TraceId), zap.Int("user_id", trace.UserId))...,
)
}
3.3 解决Caller问题
此方式存在个问题,就是打印的caller信息全都是对应日志工具类的代码行,而不是调用处。看了下zap的源码,zap打印时将整个调用的stack链路会存放到内存中,默认打印调用处的caller信息。所以为了解决解决这个问题,需要再初始化zap时额外增加AddCallerSkip跳过指定层级的caller,核心代码如下:
logger = logger.WithOptions(zap.AddCaller(),zap.AddCallerSkip(1))